# -*- coding: utf-8 -*-
"""
Created on Mon Sep 25 10:43:18 2017

@author: jack.zhou
"""
# Vector instance method: v = Vector['1', '2', '3']

from math import sqrt, acos, pi
from decimal import Decimal, getcontext

getcontext().prec = 30

class Vector(object):

    CANNOT_NORMALIZE_ZERO_VECTOR_MSG = "Cannot normalize the zero vector"
    NO_UNIQUE_PARALLEL_COMPONENT_MSG = "No unique parallel component"

    def __init__(self, coordinates):
        try:
            if not coordinates:
                raise ValueError
            self.coordinates = tuple(Decimal(x) for x in coordinates)
            self.dimension = len(coordinates)

        except ValueError:
            raise ValueError('The coordinates must be nonempty')
#            raise Exception('The coordinates must be nonempty')

        except TypeError:
            raise TypeError('The coordinates must be an iterable')
#            raise Exception('The coordinates must be an iterable')


    def plus(self, v):
        new_coordinates = [x+y for x, y in zip(self.coordinates, v.coordinates)]
        return Vector(new_coordinates)

    def minus(self, v):
        new_coordinates = [x-y for x, y in zip(self.coordinates, v.coordinates)]
        return Vector(new_coordinates)

    def times_scalar(self, c):
        new_coordinates = [c*x for x in self.coordinates]
        return Vector(new_coordinates)

    def magnitude(self):
        coordinates_squared = [x**2 for x in self.coordinates]
        return Decimal(sqrt(sum(coordinates_squared)))

    def normalized(self):
        try:
            magnitude = self.magnitude()
            return self.times_scalar(Decimal('1.0')/magnitude)
        except ZeroDivisionError:
            raise Exception('Cann\'t normalize the zero vector')

    def dot(self,v):
        return sum([x*y for x, y in zip(self.coordinates, v.coordinates)])

    def angle_with(self,v, in_degrees=False):
        try:
            u1 = self.normalized()
            u2 = v.normalized()

            val = u1.dot(u2)
            val = min(val, Decimal('1.0'))
            val = max(val, Decimal('-1.0'))
            angle_in_radians = acos(val)

            if in_degrees:
                degrees_per_radian = Decimal('180')/Decimal(pi)
                
                return angle_in_radians * degrees_per_radian
                
            else:
                return angle_in_radians

        except Exception as e:
            if str(e) == self.CANNOT_NORMALIZE_ZERO_VECTOR_MSG:
                raise Exception('Cannot get an angle with the zero vector')
            else:
                raise e

    def is_parallel_to(self, v):
        return (self.is_zero() or
                v.is_zero() or
                MyDecimal(self.angle_with(v)).is_near_zero() or
                MyDecimal(self.angle_with(v) - pi).is_near_zero())
#        ang = self.angle_with(v)
#        return (self.is_zero() or
#                v.is_zero() or
#                MyDecimal(ang).is_near_zero() or
#                MyDecimal(ang - pi).is_near_zero())


    def is_orthogonal_to(self, v, tolerance=1e-5):
        return abs(self.dot(v)) < tolerance
    

    def is_zero(self, tolerance=1e-5):
        return self.magnitude() < tolerance

    def component_parallel_to(self, basis):
        try:
            u = basis.normalized()
            weight = self.dot(u)
            return u.times_scalar(weight)
        except Exception as e:
            if str(e) == self.CANNOT_NORMALIZE_ZERO_VECTOR_MSG:
                raise Exception(self.CANNOT_NORMALIZE_ZERO_VECTOR_MSG)
            else:
                raise e

    def component_orthogonal_to(self, basis):
        try:
            projection = self.component_parallel_to(basis)
            return self.minus(projection)
        except Exception as e:
            if str(e) == self.NO_UNIQUE_PARALLEL_COMPONENT_MSG:
                raise Exception(self.NO_UNIQUE_PARALLEL_COMPONENT_MSG)
            else:
                raise e
    def cross(self,v):
        try:
            x_1, y_1, z_1 = self.coordinates
            x_2, y_2, z_2 = v.coordinates
            new_coordinates = [ y_1*z_2 - y_2*z_1,
                                -(x_1*z_2 - x_2*z_1),
                                x_1*y_2 - x_2*y_1 ]
            return Vector(new_coordinates)
        except ValueError as e:
            msg = str(e)
            if msg == 'need more than 2 values to unpack':
                self_embeded_in_R3 = Vector(self.coordiantes + ('0',))
                v_embeded_in_R3 = Vector(v.coordinates + ('0',))
                return self_embeded_in_R3.cross(v_embeded_in_R3)
            elif (msg == 'too many values to unpack' or
                    msg == 'need more than 1 value to unpack'):
                    raise Exception(self.ONLY_DEFINED_IN_TWO_THREE_DIMS_MSG)
            else:
                raise e

    def area_of_parallelogram_with(self,v):
        cross_product = self.cross(v)
        return cross_product.magnitude()

    def area_of_triangle_with(self,v):
        area_para = self.area_of_parallelogram_with(v)
        return Decimal(area_para) / Decimal('2.0')

    def __str__(self):
        return 'Vector: {}'.format(self.coordinates)
        #return 'Vector: {}'.format((float(str(e)) for e in self.coordinates))
#        result = []
#        for e in self.coordinates:
#            result.append(float(e))
#        return 'Vector: {}'.format(tuple(result))


    def __eq__(self, v):
        for n, m in zip(self.coordinates, v.coordinates):
            if not MyDecimal(n-m).is_near_zero(eps=1e-6):
                return False
        return True
        #return self.coordinates == v.coordinates
        
    def __getitem__(self, i):
        return self.coordinates[i]    
    
class MyDecimal(Decimal):
    def is_near_zero(self, eps=1e-6):
        return abs(self) < eps


#print("-----1------")
#v = Vector(['-7.579', '-7.88'])
#w = Vector(['22.737', '23.64'])
##parallel
#print(v.is_parallel_to(w))
#print(v.is_orthogonal_to(w))
#print()
#print("-----2------")
#v = Vector(['-2.029', '9.97', '4.172'])
#w = Vector(['-9.231', '-6.639', '-7.245'])
##intersect
#print(v.is_parallel_to(w))
#print(v.is_orthogonal_to(w))
#print()
#print("-----3------")
#v = Vector(['-2.328', '-7.284', '-1.214'])
#w = Vector(['-1.821', '1.072', '-2.94'])
##orthogonal
#print(v.is_parallel_to(w))
#print(v.is_orthogonal_to(w))
#print()
#print("-----4------")
#v = Vector(['2.118', '4.827'])
#w = Vector(['0', '0'])
##paralle and orthogonal
#print(v.is_parallel_to(w))
#print(v.is_orthogonal_to(w))
#
#print("=======5=========")
#v1 = Vector(['0.999999999', '0.99999999999'])
#v2 = Vector(['1.000000', '1.00000000'])
#print(v1.__eq__(v2))
#
#print("**********6************")
#v1 = Vector(['0.0000009999','0.0000009999'])
#print(v1.is_zero())

#Due to __getitem__(),so support v[i] to get value
#v = Vector(['2.118', '4.827'])
#print(v[0])




